using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Presentation.Media;

namespace codebase
{
    public class PNG
    {

        #region Constants

        private readonly int[] lengthbase = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258 };
        private readonly int[] lengthextra = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 };
        private readonly int[] distancebase = { 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577 };
        private readonly int[] distanceextra = { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 };
        private readonly int[] clcl = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; //code length code lengths

        #endregion

        #region Variables

        private int _error;
        private int _width, _height;
        private byte _bitDepth, _colorType, _compressionMethod, _filterMethod, _interlaceMethod;
        private int _keyDefined = 0;
        private byte[] idat;
        private byte[] ipal;
        private int _rKey, _gKey, _bKey;
        private int _bpp;

        #endregion

        #region Constructors

        public PNG(byte[] Data)
        {
            byte[] b;
            byte[] bOut;
            int pos = 33; // First byte of first chunk after header
            bool bEnd = false;
            int chunkLength;

            _error = 0; // Reset error

            // Check data
            if (Data == null || Data.Length == 0)
            {
                _error = 48;
                return;
            }

            // Read Header
            b = new byte[29];
            Array.Copy(Data, b, 29);
            ReadHeader(b);
            if (_error != 0)
                return;

            Debug.Print("<<Header Decoded>>");
            Debug.Print("Dimensions: " + _width + "x" + _height);
            Debug.Print("Color Type: " + _colorType);
            Debug.Print("Bit Depth: " + _bitDepth);
            Debug.Print("Compression: " + _compressionMethod);
            Debug.Print("Filter: " + _filterMethod);
            Debug.Print("");

            _keyDefined = 0;
            while (!bEnd)
            {
                if (pos + 8 >= Data.Length)
                {
                    _error = 30;
                    return;
                }

                // Get chunk length
                chunkLength = ((int)Data[pos] << 24) + ((int)Data[pos + 1] << 16) + ((int)Data[pos + 2] << 8) + Data[pos + 3];
                pos += 4;

                if (chunkLength > 2147483647)
                {
                    _error = 63;
                    return;
                }

                if (pos + chunkLength >= Data.Length)
                {
                    _error = 35;
                    return;
                }

                if (Data[pos + 0] == 'I' && Data[pos + 1] == 'D' && Data[pos + 2] == 'A' && Data[pos + 3] == 'T') //IDAT chunk, contaDataDatag compressed image data
                {
                    b = new byte[chunkLength];
                    Array.Copy(Data, pos + 4, b, 0, chunkLength);
                    InsertIntoIDAT(b);
                    pos += 4 + chunkLength;
                }
                else if (Data[pos + 0] == 'I' && Data[pos + 1] == 'E' && Data[pos + 2] == 'N' && Data[pos + 3] == 'D')
                {
                    pos += 4;
                    bEnd = true;
                }
                else if (Data[pos + 0] == 'P' && Data[pos + 1] == 'L' && Data[pos + 2] == 'T' && Data[pos + 3] == 'E') //palette chunk (PLTE)
                {
                    pos += 4;
                    ipal = new byte[4 * (chunkLength / 3)];

                    if (ipal.Length > (4 * 256))
                    {
                        _error = 38;
                        return;
                    }

                    for (int i = 0; i < ipal.Length; i += 4)
                    {
                        for (int j = 0; j < 3; j++)
                            ipal[i + j] = Data[pos++];
                        ipal[i + 3] = 255;
                    }
                }
                else if (Data[pos + 0] == 't' && Data[pos + 1] == 'R' && Data[pos + 2] == 'N' && Data[pos + 3] == 'S') //palette transparency chunk (tRNS)
                {
                    pos += 4; //go after the 4 letters
                    if (_colorType == 3)
                    {
                        if (4 * chunkLength > ipal.Length)
                        {
                            _error = 39;
                            return;
                        } //_error: more alpha values given than there are palette entries
                        for (int i = 0; i < chunkLength; i++)
                            ipal[4 * i + 3] = Data[pos++];
                    }
                    else if (_colorType == 0)
                    {
                        if (chunkLength != 2)
                        {
                            _error = 40;
                            return;
                        } //_error: this chunk must be 2 bytes for greyscale image
                        _keyDefined = 1; _rKey = _gKey = _bKey = 256 * Data[pos] + Data[pos + 1]; pos += 2;
                    }
                    else if (_colorType == 2)
                    {
                        if (chunkLength != 6) { _error = 41; return; } //_error: this chunk must be 6 bytes for RGB image
                        _keyDefined = 1;
                        _rKey = 256 * Data[pos] + Data[pos + 1]; pos += 2;
                        _gKey = 256 * Data[pos] + Data[pos + 1]; pos += 2;
                        _bKey = 256 * Data[pos] + Data[pos + 1]; pos += 2;
                    }
                    else
                    {
                        _error = 42;
                        return;
                    } //_error: tRNS chunk not allowed for other color models
                }
                else //it's not an implemented chunk type, so ignore it: skip over the data
                {
                    if ((Data[pos + 0] & 32) <= 0)
                    {
                        _error = 69;
                        return;
                    } //error: unknown critical chunk (5th bit of first byte of chunk type is 0)
                    pos += (chunkLength + 4); //skip 4 letters and uninterpreted data of unimplemented chunk
                }
                pos += 4; //step over CRC (which is ignored)
            }

            _bpp = GetBPP();
            Debug.Print("BPP: " + _bpp);

            byte[] scanlines = new byte[((_width * (_height * _bpp + 7)) / 8) + _height];
            Zlib z = new Zlib();
            _error = z.Decompress(ref scanlines, idat);
            idat = null;
            if (_error > 0)
                return;

            int bytewidth = (_bpp + 7) / 8, outlength = (_height * _width * _bpp + 7) / 8;
            bOut = new byte[outlength];

            if (_interlaceMethod == 0)
            {
                Debug.Print("No interlace");
                int linestart = 0, linelength = (_width * _bpp + 7) / 8; //length in bytes of a scanline, excluding the filtertype byte
                if (_bpp >= 8) //byte per byte
                {
                    Debug.Print("BPP > 8");
                    for (int y = 0; y < _height; y++)
                    {
                        int filterType = scanlines[linestart];
                        int prevline = (y == 0) ? 0 : ((y - 1) * _width * bytewidth);
                        unFilterScanline(ref bOut, scanlines, prevline, bytewidth, filterType, linelength, linestart - y, linestart + 1);
                        if (_error != 0)
                            return;
                        linestart += (1 + linelength); //go to start of next scanline
                    }
                }
                else
                {
                    Debug.Print("BPP <= 8");
                }
            }
            else
            {
                Debug.Print("Adam 7 interlacing");
            }

            if (_colorType != 6 || _bitDepth != 8) //conversion needed
            {
                Debug.Print("Conversion Needed");
                _error = Convert(ref b, ref bOut);
                Debug.Print("Error: " + _error);
            }

            scanlines = null;
            Bitmap bmp = new Bitmap(_width, _height);
            int c = 0;

            if (_bpp == 24)
            {
                for (int y = 0; y < _height; y++)
                {
                    for (int x = 0; x < _width; x++)
                    {
                        Color cl = ColorUtility.ColorFromRGB(bOut[c], bOut[c + 1], bOut[c + 2]);
                        c += 3;
                        bmp.SetPixel(x, y, cl);
                    }
                }
            }
            else if (_bpp == 32)
            {
                for (int y = 0; y < _height; y++)
                {
                    for (int x = 0; x < _width; x++)
                    {
                        Color cl = ColorUtility.ColorFromRGB(bOut[c], bOut[c + 1], bOut[c + 2]);
                        c += 4;
                        bmp.SetPixel(x, y, cl);
                    }
                }
            }

            Bitmap bmp2 = new Bitmap(320, 240);
            Color xl = ColorUtility.ColorFromRGB(255, 0, 0);
            bmp2.DrawRectangle(0, 0, 0, 0, 320, 240, 0, 0, xl, 0, 0, xl, 0, 0, 256);
            bmp2.DrawImage(160 - _width / 2, 120 - _height / 2, bmp, 0, 0, _width, _height);
            bmp2.Flush();
        }

        #endregion

        #region Properties

        public int Error
        {
            get { return _error; }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Check for the validity of a Type/Depth combination
        /// </summary>
        /// <param name="ColorType"></param>
        /// <param name="BitDepth"></param>
        /// <returns></returns>
        private int CheckColorValidity(byte ColorType, byte BitDepth)
        {
            if ((ColorType == 2 || ColorType == 4 || ColorType == 6)) if (!(BitDepth == 8 || BitDepth == 16))
                    return 37;
                else if (ColorType == 0) if (!(BitDepth == 1 || BitDepth == 2 || BitDepth == 4 || BitDepth == 8 || BitDepth == 16))
                        return 37;
                    else if (ColorType == 3) if (!(BitDepth == 1 || BitDepth == 2 || BitDepth == 4 || BitDepth == 8))
                            return 37;
                        else
                            return 31; //unexisting color type
            return 0; //allowed color type / bits combination
        }

        private int Convert(ref byte[] bOut, ref byte[] bIn)
        {
            int numpixels = _width * _height, bp = 0;

            bOut = new byte[numpixels * 4];

            if (_bitDepth == 8 && _colorType == 0) //greyscale
                for (int i = 0; i < numpixels; i++)
                {
                    bOut[4 * i + 0] = bOut[4 * i + 1] = bOut[4 * i + 2] = bIn[i];
                    bOut[4 * i + 3] = (byte)(((_keyDefined == 1) && (bIn[i] == _rKey)) ? 0 : 255);
                }
            else if (_bitDepth == 8 && _colorType == 2) //RGB color
                for (int i = 0; i < numpixels; i++)
                {
                    for (int c = 0; c < 3; c++) bOut[4 * i + c] = bIn[3 * i + c];
                    bOut[4 * i + 3] = (byte)((_keyDefined == 1 && bIn[3 * i + 0] == _rKey && bIn[3 * i + 1] == _gKey && bIn[3 * i + 2] == _bKey) ? 0 : 255);
                }
            else if (_bitDepth == 8 && _colorType == 3) //indexed color (palette)
                for (int i = 0; i < numpixels; i++)
                {
                    if (4U * bIn[i] >= ipal.Length) return 46;
                    for (int c = 0; c < 4; c++) bOut[4 * i + c] = ipal[4 * bIn[i] + c]; //get rgb colors from the palette
                }
            else if (_bitDepth == 8 && _colorType == 4) //greyscale with alpha
                for (int i = 0; i < numpixels; i++)
                {
                    bOut[4 * i + 0] = bOut[4 * i + 1] = bOut[4 * i + 2] = bIn[2 * i + 0];
                    bOut[4 * i + 3] = bIn[2 * i + 1];
                }
            else if (_bitDepth == 8 && _colorType == 6) for (int i = 0; i < numpixels; i++) for (int c = 0; c < 4; c++) bOut[4 * i + c] = bIn[4 * i + c]; //RGB with alpha
            else if (_bitDepth == 16 && _colorType == 0) //greyscale
                for (int i = 0; i < numpixels; i++)
                {
                    bOut[4 * i + 0] = bOut[4 * i + 1] = bOut[4 * i + 2] = bIn[2 * i];
                    bOut[4 * i + 3] = (byte)((_keyDefined == 1 && 256U * bIn[i] + bIn[i + 1] == _rKey) ? 0 : 255);
                }
            else if (_bitDepth == 16 && _colorType == 2) //RGB color
                for (int i = 0; i < numpixels; i++)
                {
                    for (int c = 0; c < 3; c++) bOut[4 * i + c] = bIn[6 * i + 2 * c];
                    bOut[4 * i + 3] = (byte)((_keyDefined == 1 && 256U * bIn[6 * i + 0] + bIn[6 * i + 1] == _rKey && 256U * bIn[6 * i + 2] + bIn[6 * i + 3] == _gKey && 256U * bIn[6 * i + 4] + bIn[6 * i + 5] == _bKey) ? 0 : 255);
                }
            else if (_bitDepth == 16 && _colorType == 4) //greyscale with alpha
                for (int i = 0; i < numpixels; i++)
                {
                    bOut[4 * i + 0] = bOut[4 * i + 1] = bOut[4 * i + 2] = bIn[4 * i]; //most significant byte
                    bOut[4 * i + 3] = bIn[4 * i + 2];
                }
            else if (_bitDepth == 16 && _colorType == 6) for (int i = 0; i < numpixels; i++) for (int c = 0; c < 4; c++) bOut[4 * i + c] = bIn[8 * i + 2 * c]; //RGB with alpha
            else if (_bitDepth < 8 && _colorType == 0) //greyscale
                for (int i = 0; i < numpixels; i++)
                {
                    int value = (readBitsFromReversedStream(ref bp, ref bIn, _bitDepth) * 255) / ((1 << _bitDepth) - 1); //scale value from 0 to 255
                    bOut[4 * i + 0] = bOut[4 * i + 1] = bOut[4 * i + 2] = (byte)(value);
                    bOut[4 * i + 3] = (byte)((_keyDefined == 1 && value != 0 && (((1U << _bitDepth) - 1U) == _rKey) && ((1U << _bitDepth) - 1U) != 0) ? 0 : 255);
                }
            else if (_bitDepth < 8 && _colorType == 3) //palette
                for (int i = 0; i < numpixels; i++)
                {
                    int value = readBitsFromReversedStream(ref bp, ref bIn, _bitDepth);
                    if (4 * value >= ipal.Length) return 47;
                    for (int c = 0; c < 4; c++) bOut[4 * i + c] = ipal[4 * value + c]; //get rgb colors from the palette
                }
            return 0;
        }

        private int GetBPP()
        {
            if (_colorType == 2) return (3 * _bitDepth);
            else if (_colorType >= 4) return (_colorType - 2) * _bitDepth;
            else return _bitDepth;
        }

        /// <summary>
        /// Append an array to the IDAT array
        /// </summary>
        /// <param name="b"></param>
        private void InsertIntoIDAT(byte[] b)
        {
            if (idat == null)
                idat = b;
            else
            {
                byte[] tmp = new byte[idat.Length + b.Length];
                Array.Copy(idat, tmp, idat.Length);
                Array.Copy(b, 0, tmp, idat.Length, b.Length);
                idat = tmp;
            }
        }

        private long paethPredictor(long a, long b, long c) //Paeth predicter, used by PNG filter type 4
        {
            long p = a + b - c, pa = p > a ? p - a : a - p, pb = p > b ? p - b : b - p, pc = p > c ? p - c : c - p;
            return (pa <= pb && pa <= pc) ? a : pb <= pc ? b : c;
        }

        private int readBitFromReversedStream(ref int bitp, ref byte[] bits)
        {
            int result = (bits[bitp >> 3] >> (7 - bitp & 0x7)) & 1;
            bitp++;
            return result;
        }

        private int readBitsFromReversedStream(ref int bitp, ref byte[] bits, int nbits)
        {
            int result = 0;
            for (int i = nbits - 1; i < nbits; i--) result += ((readBitFromReversedStream(ref bitp, ref bits)) << i);
            return result;
        }

        /// <summary>
        /// Reads the PNG's header data
        /// </summary>
        /// <param name="b"></param>
        private void ReadHeader(byte[] b)
        {
            // Check length
            if (b.Length < 29)
            {
                _error = 27;
                return;
            }

            // Check PNG signature
            if (b[0] != 137 || b[1] != 80 || b[2] != 78 || b[3] != 71 || b[4] != 13 || b[5] != 10 || b[6] != 26 || b[7] != 10)
            {
                _error = 28;
                return;
            }

            // Check IHDR signature
            if (b[12] != 'I' || b[13] != 'H' || b[14] != 'D' || b[15] != 'R')
            {
                _error = 28;
                return;
            }

            // Read Info
            _width = ((int)b[16] << 24) + ((int)b[17] << 16) + ((int)b[18] << 8) + b[19];
            _height = ((int)b[20] << 24) + ((int)b[21] << 16) + ((int)b[22] << 8) + b[23];
            _bitDepth = b[24];
            _colorType = b[25];

            _compressionMethod = b[26];
            if (_compressionMethod != 0)
            {
                _error = 32;
                return;
            }

            _filterMethod = b[27];
            if (_filterMethod != 0)
            {
                _error = 33;
                return;
            }

            _interlaceMethod = b[28];
            if (_interlaceMethod > 1)
            {
                _error = 34;
                return;
            }

            _error = CheckColorValidity(_colorType, _bitDepth);
        }

        private void unFilterScanline(ref byte[] recon, byte[] scanline, int precon, int bytewidth, int filterType, int length, int offset1, int offset2)
        {
            int i;

            //Debug.Print("Filter: " + filterType);
            switch (filterType)
            {
                case 0: // None
                    for (i = 0; i < length; i++)
                        recon[offset1 + i] = scanline[offset2 + i];
                    break;
                case 1:
                    for (i = 0; i < bytewidth; i++)
                        recon[offset1 + i] = scanline[offset2 + i];
                    for (i = bytewidth; i < length; i++)
                        recon[offset1 + i] = (byte)(scanline[offset2 + i] + recon[offset1 + i - bytewidth]);
                    break;
                case 2: // Sub
                    if (precon > 0)
                    {
                        for (i = 0; i < length; i++)
                            recon[offset1 + i] = (byte)(scanline[offset2 + i] + recon[precon + i]);
                    }
                    else
                    {
                        for (i = 0; i < length; i++)
                            recon[offset1 + i] = scanline[offset2 + i];
                    }
                    break;
                case 3:
                    if (precon > 0)
                    {
                        for (i = 0; i < bytewidth; i++)
                            recon[offset1 + i] = (byte)(scanline[offset2 + i] + recon[precon + i] / 2);
                        for (i = bytewidth; i < length; i++)
                            recon[offset1 + i] = (byte)(scanline[offset2 + i] + ((recon[offset1 + i - bytewidth] + recon[precon + i]) / 2));
                    }
                    else
                    {
                        for (i = 0; i < bytewidth; i++)
                            recon[offset1 + i] = scanline[offset2 + i];
                        for (i = bytewidth; i < length; i++)
                            recon[offset1 + i] = (byte)(scanline[offset2 + i] + recon[offset1 + i - bytewidth] / 2);
                    }
                    break;
                case 4: // Paeth
                    if (precon > 0)
                    {
                        for (i = 0; i < bytewidth; i++)
                            recon[offset1 + i] = (byte)(scanline[offset2 + i] + paethPredictor(0, recon[precon + i], 0));
                        for (i = bytewidth; i < length; i++)
                            recon[offset1 + i] = (byte)(scanline[offset2 + i] + paethPredictor(recon[offset1 + i - bytewidth], recon[precon + i], recon[precon + i - bytewidth]));
                    }
                    else
                    {
                        for (i = 0; i < bytewidth; i++)
                            recon[offset1 + i] = scanline[offset2 + i];
                        for (i = bytewidth; i < length; i++)
                            recon[offset1 + i] = (byte)(scanline[offset2 + i] + paethPredictor(recon[offset1 + i - bytewidth], 0, 0));
                    }
                    break;
                default:
                    _error = 36;
                    return; //error: unexisting filter type given
            }
        }

        #endregion

    }
}
